/*
 * Copyright (c) 2010, Yawning <yawninglol at gmail.com>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *   * Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *
 *   * Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *
 *   * Neither the name of the Mew Developers nor the
 *     names of its contributors may be used to endorse or promote products
 *     derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

package yawning.mew.cat;

import java.util.EnumSet;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;

import yawning.mew.BuffsDebuffs.Buff;
import yawning.mew.BuffsDebuffs.Debuff;
import yawning.mew.Formulation;
import yawning.mew.MewView;
import yawning.mew.ProcEffect;
import yawning.mew.ProcEffect.Effect;
import yawning.mew.Procs;
import yawning.mew.PropertiesBase;
import yawning.mew.PropertiesBase.IProperty;
import yawning.mew.Report;
import yawning.mew.character.Druid.Glyph;
import yawning.mew.character.Druid.Talent;
import yawning.mew.character.Toon;
import yawning.mew.sim.SimulationManager.Property;

/**
 * DPS Model based on the WotLK Toskk DPS calculator.
 *
 * @author Yawning <yawninglol at gmail.com>
 */
public class CatModelToskk implements Formulation {
    public enum ToskkProperty implements IProperty {
        OOCSHREDONLY("Toskk.OoCShredOnly", "false"),                // Always Shred on Clearcast
        USEMAXSR("Toskk.UseMaxSR", "false"),                        // SR at 5 points (or minimum to maintain cycle)
        USEMANGLE("Toskk.UseMangle", "false"),                      // Mangle spam cycle.
        ASSUMEEXTERNALBLEED("Tossk.AssumeExternalBleed", "false"),  // Mob is always bleeding for purposes of Rend and Tear.
        ASSUMEEXTERNALFF("Toskk.AssumeExternalFF", "false"),        // Mob is always fully debuffed with the MajorArmor debuff.
        ASSUMEEXTERNALMANGLE("Toskk.AssumeExternalMangle", "false"),// Mob is always fully debuffed with the Bleed Damage debuff.
        MODELOPENER("Toskk.ModelOpener", "false");                  // Model the opener.

        private final String propertyKey;
        private final String defaultValue;

        private ToskkProperty(String k) {
            this(k, null);
        }

        private ToskkProperty(String k, String v) {
            propertyKey = k;
            defaultValue = v;
        }

        public String getKey() {
            return this.propertyKey;
        }

        public String getDefault() {
            return this.defaultValue;
        }
    }

    class ToskkProperties extends PropertiesBase {
        public ToskkProperties(Properties p) {
            super(p);
        }

        public boolean isAssumeExternalBleed() {
            return getBool(ToskkProperty.ASSUMEEXTERNALBLEED);
        }

        public boolean isAssumeExternalFF() {
            return getBool(ToskkProperty.ASSUMEEXTERNALFF);
        }

        public boolean isAssumeExternalMangle() {
        	return getBool(ToskkProperty.ASSUMEEXTERNALMANGLE);
        }

        public boolean isModelOpener() {
            return getBool(ToskkProperty.MODELOPENER);
        }

        public boolean isOoCShredOnly() {
            return getBool(ToskkProperty.OOCSHREDONLY);
        }

        public boolean isUseMangle() {
            return getBool(ToskkProperty.USEMANGLE);
        }

        public boolean isUseMaxSR() {
            return getBool(ToskkProperty.USEMAXSR);
        }

        @Override
        public String toString() {
            return valuesToString(ToskkProperty.values());
        }
    }

    public Report calculate(Toon toon, PropertiesBase config, MewView view) {
    	Implementation impl = new Implementation(toon, config);
        impl.toskkProps = new ToskkProperties(config.getProperties());
        impl.view = view;
        return impl.calculate();
    }

    private class Implementation extends CatModel {
    	Implementation(Toon toon, PropertiesBase config) {
    		super(toon, config);
    	}

    	public Report calculate() {
            this.encounterDuration = config.getInt(Property.SIMULATION_DURATION); // XXX: Probably this config should be moved to Mew
            this.bitwTime = getBloodInTheWaterTime();

            // Validate our parameters.
            if (validateStats() == false) {
                return null;
            }

            // Formulate heroism/unholy frenzy.
            formulateBuffs();

            // Derive our hit table and what not.
//            calculateEffectiveStats();

            // Calculate buff debuff durations, berserk uptime, energy costs.
            deriveCycleParams();

            // Calculate the cycle to obtain proc trigger information.
            //
            // Note: Proc triggers are calculated for the non-Blood in the Water rotation.
            // Unless the druid is massively undergeared, this should not make any difference.
            CycleResults base = calculateCycle();

            // Calculate proc effects and factor them into our stats.
            calculateProcEffects(base);

            // Recalculate stats, cycle parameters, cycle and DPS.
//            calculateEffectiveStats();
            deriveCycleParams();
            CycleResults withProcs = calculateCycle();
            CycleDPS procDPS = calculateCycleDPS(withProcs);

            // Write our report.
            Report r  = generateReport(withProcs, procDPS);
            r.setPerformance(procDPS.dps);

            return r;
    	}

	    //
	    // Actual model implementation.
	    //

	    private ToskkProperties toskkProps;

	    private int encounterDuration;
	    private double bitwTime;

	    // Weapon stats, combat table.
	    private double pFail;
	    private double procDamagePhys = 0.0;
	    private double procDamageSpell = 0.0;

	    // Proc uptimes. (For reporting)
	    private HashMap<ProcEffect, Double> procUptimes = null;

	    private double rakeTicks;
	    private double segmentRipDuration;
	    private double ripTicks;
	    private double fEnergyRegenRate;

	    private int berserkUpDuration;
	    private double berserkUptime;

	    private int tigersFuryUpDuration;
	    private double tigersFuryUptime;

	    /**
	     * Check to ensure that our parameters are sane.
	     */
	    private boolean validateStats() {
	        // We require 2/2 Primal Fury.
	        if (druid.getTalent(Talent.PRIMALFURY) != 2) {
	            this.view.onError("2/2 Primal Fury is required.");
	            return false;
	        }

	        // Blood in the Water is 0/2 or 2/2 only.
	        if (bitwTime > 0 ) {
	            if (druid.getTalent(Talent.BLOODINTHEWATER) == 1) {
	                this.view.onError("Blood in the Water modeling requires 2 points in the talent.");
	                return false;
	            }
	        }

	        // Stampede is 0/2 or 2/2 only.  (Don't need to alter opener modeling if I do that.)
	        if (druid.getTalent(Talent.STAMPEDE) == 1) {
	        	this.view.onError("Stampede modeling requires 2 points in the talent.");
	        	return false;
	        }

	        // Require Bleed Damage to be enabled.
	        if (buffsDebuffs.isDebuff(Debuff.BLEEDDMG) == false) {
	        	this.view.onError("Debuff.BleedDamage is required.");
	        	return false;
	        }

	        return true;
	    }

	    /**
	     * Formulate buffs so that we can calculate effective stats.
	     */
	    private void formulateBuffs() {
	        if (buffsDebuffs.isBuff(Buff.MAJORHASTE)) {
	            int heroismUpDuration = GetCooldownUptime(heroismBuffDuration, heroismCDDuration, encounterDuration);
	            double heroismUptime = (double) heroismUpDuration / encounterDuration;
//	            buffHeroism = 1.3 * heroismUptime + (1 - heroismUptime);
	            hasteMult *= 1.3 * heroismUptime + (1 - heroismUptime);
	        }

	        if (buffsDebuffs.isBuff(Buff.UNHOLYFRENZY)) {
	            int unholyFrenzyUpDuration = GetCooldownUptime(unholyFrenzyBuffDuration, unholyFrenzyCDDuration, encounterDuration);
	            double unholyFrenzyUptime = (double) unholyFrenzyUpDuration / encounterDuration;
//	            buffUnholyFrenzy = 1.2 * unholyFrenzyUptime + (1 - unholyFrenzyUptime);
	            meleeHasteMult *= 1.2 * unholyFrenzyUptime + (1 - unholyFrenzyUptime);
	        }

	        if (buffsDebuffs.isDebuff(Debuff.SHATTERINGTHROW)) {
	        	int shatteringThrowUpDuration = GetCooldownUptime(shatteringThrowDebuffDuration, shatteringThrowCDDuration, encounterDuration);
	        	double shatteringThrowUptime = (double) shatteringThrowUpDuration / encounterDuration;
	        	debuffShatteringThrow = 0.8 * shatteringThrowUptime + (1 - shatteringThrowUptime);
	        }
	    }

	    /**
	     * Derive things like buff debuff duration, ability costs, energy regeneration.
	     */
	    private void deriveCycleParams() {
	        this.pFail = getMissChance() + getDodgeChance();

	        // Rake/Rip Durations.
	        this.rakeTicks = getRakeDebuffDuration() / getRakeTickInterval();
	        segmentRipDuration = getRipDebuffDuration();
	        if (druid.hasGlyph(Glyph.SHRED) && toskkProps.isUseMangle() == false) {
	            segmentRipDuration += 6;
	        }
	        this.ripTicks = segmentRipDuration / getRipTickInterval();

	        // Energy regeneration.
	        //
	        // Note: We figure out KotJ uptime/Berserk uptime and treat each talent as a static boost to energy regen.
	        // this ends up undervaluing the talents slightly since they get little synergy.
	        this.fEnergyRegenRate = getEnergyRegenRate();

	        this.berserkUpDuration = GetCooldownUptime(berserkBuffDuration, berserkCDDuration, encounterDuration);
	        this.berserkUptime = (double) berserkUpDuration / encounterDuration;

	        this.fEnergyRegenRate += druid.getTalent(Talent.KINGOFTHEJUNGLE) * 20 / tigersFuryCDDuration;
	        this.fEnergyRegenRate += ((druid.getTalent(Talent.BERSERK) > 0) ? fEnergyRegenRate * berserkUptime : 0);

	        // Primal Madness provides +10/20e on Berserk/TF use.
	        // XXX: This overvalues the talent on the assumption that it is possible to consume all energy.
	        if (druid.getTalent(Talent.PRIMALMADNESS) > 0) {
	            double primalMadnessEnergyRegenRate = 0;

	            primalMadnessEnergyRegenRate += (double) primalMadnessEnergyGain / tigersFuryCDDuration;
	            if (druid.getTalent(Talent.BERSERK) > 0) {
	                primalMadnessEnergyRegenRate += (double) primalMadnessEnergyGain / berserkCDDuration;
	            }

	            this.fEnergyRegenRate += primalMadnessEnergyRegenRate;
	        }

	        oocPPM = 60 / ((1 / (3.5 / 60)) / ((1 - pFail) / getMeleeInterval())); // PPM based, only triggered by white attacks.
	    }

	    class CycleResults {
	        double cycleTime = 0;

	        double numberWhites = 0;
	        double numberFurySwipes = 0;
	        double numberMangles = 0;
	        double numberRakes = 0;
	        double numberShreds = 0;
	        double numberRips = 0;
	        double numberRoars = 0;
	        double numberBites = 0;

	        double roarUptime = 0;
	        double ripUptime = 0;
	        double rakeUptime = 0;
	        double mangleUptime = 0;
	        double totalAttacks = 0;

	        int roarCp = 0;

	        double energyRegenRate = 0;
	        double oocProcReturn = 0;

	        double requiredAttacks = 0;
	        double numberAttacks = 0;
	        boolean cycleWarning = false;

	        double tier_11_4pcUptime = 0;
	    }

	    /**
	     * Calculate the base cycle.
	     */
	    private CycleResults calculateCycle() {
	        CycleResults rval = new CycleResults();

	        double pCrit = getMeleeCritChance();

	        // Results we want to pass back to the user.
	        double cycleTime = 0;
	        double cycleEnergyRegenRate = fEnergyRegenRate;
	        double numberFurySwipes = 0;
	        double numberMangles = 0;
	        double numberRakes = 0;
	        double numberShreds = 0;
	        double numberRips = 0;
	        double numberRoars = 0;
	        double numberBites = 0;

	        double roarUptime = 0;
	        double ripUptime = 0;
	        double rakeUptime = 0;
	        double mangleUptime = 0;

	        double tier_11_4pcUptime = 0;

	        int roarCp = 0;

	        double oocProcReturn = 0;

	        double requiredAttacks = 0;
	        double numberAttacks = 0;
	        boolean cycleWarning = false;

	        // Calculate number of Rips and uptime
	        numberRips = 1 / (1 - pFail);
	        double ripSegmentTime = (1 / (1 - pFail) * ripCost * ((1 - pFail) + 0.2 * pFail) - ripCost) / getEnergyRegenRate() + segmentRipDuration; // Total time between Rips, if Energy is limiting
	        double ripGcdTime = 1 / (1 - pFail) - 1 + segmentRipDuration; // Total time between Rips, if GCDs are limiting
	        ripSegmentTime = Math.max(ripSegmentTime, ripGcdTime);
	        cycleTime = ripSegmentTime;
	        ripUptime = segmentRipDuration / cycleTime;
	        requiredAttacks = (5 - 4 * pCrit + 3 * Math.pow(pCrit, 2) - 2 * Math.pow(pCrit, 3) + Math.pow(pCrit, 4)) / (1 - pFail);

	        // Calculate number of Rakes and uptime
	        double rakeRipOverlap = 1 + 1 / (1 - pFail);
	        double rakeSegmentTime = 1 / (1 - pFail) - 1 + getRakeDebuffDuration(); // Total time between Rakes, limited by GCDs
	        double rakeDowntime = rakeRipOverlap / rakeSegmentTime * rakeRipOverlap / 2; // Average Rake uptime cost per Rip
	        numberRakes = (cycleTime - rakeDowntime) / rakeSegmentTime / (1 - pFail);
	        rakeUptime = (numberRakes * (1 - pFail) * getRakeDebuffDuration()) / cycleTime;

	        // Calculate number of Roars and uptime
	        double highCpRoar = 0;
	        double roarSegmentTime = 0;

	        if (toskkProps.isUseMaxSR() == false) {
	            numberRoars = 1;

	            if ((roarInitialDuration + roarCpBonus) >= ripSegmentTime) {
	                roarCp = 1;
	                highCpRoar = pCrit;
	                requiredAttacks = requiredAttacks +  1 / (1 - pFail);
	            } else if ((roarInitialDuration + roarCpBonus * 2) >= ripSegmentTime) {
	                roarCp = 2;
	                highCpRoar = pCrit - Math.pow(pCrit, 2);
	                requiredAttacks = requiredAttacks + (2 - pCrit) / (1 - pFail);
	            } else if ((roarInitialDuration + roarCpBonus * 3) >= ripSegmentTime) {
	                roarCp = 3;
	                highCpRoar = pCrit - Math.pow(pCrit, 2) + Math.pow(pCrit, 3);
	                requiredAttacks = requiredAttacks + (3 - 2 * pCrit + Math.pow(pCrit, 2)) / (1 - pFail);
	            } else if ((roarInitialDuration + roarCpBonus * 4) >= ripSegmentTime) {
	                roarCp = 4;
	                highCpRoar = pCrit - Math.pow(pCrit, 2) + Math.pow(pCrit, 3) - Math.pow(pCrit, 4);
	                requiredAttacks = requiredAttacks + (4 - 3 * pCrit + 2 * Math.pow(pCrit, 2) - Math.pow(pCrit, 3)) / (1 - pFail);
	            } else if ((roarInitialDuration + roarCpBonus * 5) >= ripSegmentTime) {
	                roarCp = 5;
	                highCpRoar = 0;
	                requiredAttacks = requiredAttacks + (5 - 4 * pCrit + 3 * Math.pow(pCrit, 2) - 2 * Math.pow(pCrit, 3) + Math.pow(pCrit, 4)) / (1 - pFail);
	            }

	            // XXX: I should assert that I've found a suitable SR length, though 5 PT SR will always cover a 5 pt Rip.

	            roarSegmentTime = (roarInitialDuration + roarCpBonus * (roarCp + 1)) * highCpRoar + (roarInitialDuration + roarCpBonus * roarCp) * (1 - highCpRoar);
	        } else {
	            roarCp = 5;
	            highCpRoar = 0;
	            roarSegmentTime = roarInitialDuration + roarCp * roarCpBonus;
	            double roarRakeOverlap = 1 + 1 / (1 - pFail);
	            double roarDowntime = roarRakeOverlap / roarSegmentTime * roarRakeOverlap / 2;
	            roarDowntime = roarDowntime + ripCost * ((1 - pFail) + 0.2 * pFail) / getEnergyRegenRate() / (roarSegmentTime / 2);
	            numberRoars = cycleTime / (roarSegmentTime - roarDowntime);
	            requiredAttacks = requiredAttacks + numberRoars * (5 - 4 * pCrit + 3 * Math.pow(pCrit, 2) - 2 * Math.pow(pCrit, 3) + Math.pow(pCrit, 4)) / (1 - pFail);
	        }

	        roarUptime = Math.min(1, numberRoars * roarSegmentTime / cycleTime);

	        // Calculate number of Mangles and uptime
	        if (toskkProps.isUseMangle()) {
	            numberMangles = 0; // We will derive this shortly.
	            mangleUptime = 1;
	        } else {
	            if (toskkProps.isAssumeExternalMangle() && druid.tier_11_4pc == false) {
	                numberMangles = 0;
	                mangleUptime = 1;
	            } else {
	                double mangleSegmentTime = 1 / (1 - pFail) - 1 + mangleDebuffDuration;
	                double mangleDowntime = mangleSegmentTime - mangleDebuffDuration;

	                if (druid.tier_11_4pc == true) {
	                    double mangle4t11SegmentTime = 1 / (1 - pFail) - 1 + tier_11_4pc_buffDuration;
	                    double mangle4t11Downtime = mangle4t11SegmentTime - tier_11_4pc_buffDuration;

	                    double mangleRipOverlap = 1 + 1 / (1 - pFail);
	                    mangle4t11Downtime = mangleRipOverlap / mangle4t11SegmentTime * mangleRipOverlap / 2;

	                    double mangleRoarOverlap = 1 + 1 / (1 - pFail);
	                    mangle4t11Downtime = mangle4t11Downtime + numberRoars * mangleRoarOverlap / mangle4t11SegmentTime * mangleRoarOverlap / 2;

	                    double mangleRakeOverlap = 1 + 1 / (1 - pFail);
	                    mangle4t11Downtime = mangle4t11Downtime + mangleRakeOverlap / 2 * numberRakes / (mangle4t11SegmentTime / 3);

	                    numberMangles = cycleTime / (mangle4t11SegmentTime - mangle4t11Downtime) / (1 - pFail);

	                    tier_11_4pcUptime = Math.min(1, (numberMangles * (1 - pFail) * tier_11_4pc_buffDuration) / cycleTime);

	                    // Since we are Mangling to maintain a buff that lasts 30 sec instead of 60, Mangle will have 100% uptime.
	                    mangleUptime = 1.0;
	                } else {
	                    double mangleRipOverlap = 1 + 1 / (1 - pFail);
	                    mangleDowntime = mangleRipOverlap / mangleSegmentTime * mangleRipOverlap / 2;

	                    double mangleRoarOverlap = 1 + 1 / (1 - pFail);
	                    mangleDowntime = mangleDowntime + numberRoars * mangleRoarOverlap / mangleSegmentTime * mangleRoarOverlap / 2;

	                    double mangleRakeOverlap = 1 + 1 / (1 - pFail);
	                    mangleDowntime = mangleDowntime + mangleRakeOverlap / 2 * numberRakes / (mangleSegmentTime / 3);

	                    numberMangles = cycleTime / (mangleSegmentTime - mangleDowntime) / (1 - pFail);

	                    mangleUptime = Math.min(1, (numberMangles * (1 - pFail) * mangleDebuffDuration) / cycleTime);
	                }
	            }
	        }

	        // Calculate number of Fury Swipes procs
	        if (druid.getTalent(Talent.FURYSWIPES) > 0) {
	            // 6 sec ICD, 4/8/12% proc rate.  Logs as yellow damage, does not glance.
	            numberFurySwipes = cycleTime / (furySwipesCDDuration + 1 / ((1 - pFail) / getMeleeInterval() * pFurySwipes));
	        }

	        // Calculate Energy costs and gains
	        double totalEnergy = cycleTime * cycleEnergyRegenRate;
	        double energyCost = (numberMangles * mangleCost + numberRakes * rakeCost + numberRips * ripCost + numberBites * biteCost) * ((1 - pFail) + 0.2 * pFail) + numberRoars * srCost;
	        double energyLeft = totalEnergy - energyCost;

	        if (energyLeft > 0) {
	            if (toskkProps.isUseMangle()) {
	                numberMangles = energyLeft / (mangleCost * ((1 - pFail) + 0.2 * pFail));
	            } else {
	                numberShreds = energyLeft / (shredCost * ((1 - pFail) + 0.2 * pFail));
	            }
	        }

	        if (toskkProps.isOoCShredOnly()) {
	            oocProcReturn = shredCost * ((1 - pFail) + 0.2 * pFail);
	        } else {
	            oocProcReturn = ((mangleCost * numberMangles + rakeCost * numberRakes + shredCost * numberShreds + ripCost * numberRips + biteCost * numberBites) * ((1 - pFail) + 0.2 * pFail)) / (numberMangles + numberRakes + numberShreds + numberRips + numberBites);
	        }

	        double oocBerserkProcReturn = 0;
	        if (druid.getTalent(Talent.BERSERK) > 0) {
	            if (toskkProps.isOoCShredOnly()) {
	                oocBerserkProcReturn = shredCost / 2 * ((1 - pFail) + 0.2 * pFail);
	            } else {
	                oocBerserkProcReturn = ((mangleCost / 2 * numberMangles + rakeCost / 2 * numberRakes + shredCost / 2 * numberShreds + ripCost / 2  * numberRips + biteCost / 2 * numberBites) * ((1 - pFail) + 0.2 * pFail)) / (numberMangles + numberRakes + numberShreds + numberRips + numberBites);
	            }

	            oocProcReturn = (oocProcReturn * (encounterDuration - berserkUpDuration) + oocBerserkProcReturn * berserkUpDuration) / encounterDuration;
	        }

	        cycleEnergyRegenRate = cycleEnergyRegenRate + oocPPM / 60 * oocProcReturn;
	        energyLeft = cycleTime * cycleEnergyRegenRate - energyCost;

	        if (energyLeft > 0) {
	            if (toskkProps.isUseMangle()) {
	                numberMangles = energyLeft / (mangleCost * ((1 - pFail) + 0.2 * pFail));
	            } else {
	                numberShreds = energyLeft / (shredCost * ((1 - pFail) + 0.2 * pFail));
	            }
	        }

	        // Calculate Combo Point requirement
	        numberAttacks = numberMangles + numberRakes + numberShreds;

	        if (numberAttacks > requiredAttacks) {
	            double requiredBiteAttacks = (5 - 4 * pCrit + 3 * Math.pow(pCrit, 2) - 2 * Math.pow(pCrit, 3) + Math.pow(pCrit, 4)) / (1 - pFail);

	            numberBites = (numberAttacks - requiredAttacks) / requiredBiteAttacks;

	            if (toskkProps.isUseMangle()) {
	                numberMangles = numberMangles - numberBites * biteCost * ((1 - pFail) + 0.2 * pFail) / (mangleCost * ((1 - pFail) + 0.2 * pFail));
	            } else {
	                numberShreds = numberShreds - numberBites * biteCost * ((1 - pFail) + 0.2 * pFail) / (shredCost * ((1 - pFail) + 0.2 * pFail));
	            }

	            numberAttacks = numberMangles + numberRakes + numberShreds;

	            // XXX: I'm not sure if I should be clamping this to 1/cycle.
	            numberBites = Math.min(1, (numberAttacks - requiredAttacks) / requiredBiteAttacks);
	            requiredAttacks = requiredAttacks +  numberBites * (5 - 4 * pCrit + 3 * Math.pow(pCrit, 2) - 2 * Math.pow(pCrit, 3) + Math.pow(pCrit, 4)) / (1 - pFail);
	        }

	        // Recalculate Energy costs and gains
	        cycleEnergyRegenRate = cycleEnergyRegenRate - oocPPM / 60 * oocProcReturn;
	        totalEnergy = cycleTime * cycleEnergyRegenRate;

	        if (toskkProps.isUseMangle()) {
	            energyCost = (numberRakes * rakeCost + numberRips * ripCost + numberBites * biteCost) * ((1 - pFail) + 0.2 * pFail) + numberRoars * srCost;
	        } else {
	            energyCost = (numberMangles * mangleCost + numberRakes * rakeCost + numberRips * ripCost + numberBites * biteCost) * ((1 - pFail) + 0.2 * pFail) + numberRoars * srCost;
	        }

	        if (energyLeft > 0) {
	            if (toskkProps.isUseMangle()) {
	                numberMangles = energyLeft / (mangleCost * ((1 - pFail) + 0.2 * pFail));
	            } else {
	                numberShreds = energyLeft / (shredCost * ((1 - pFail) + 0.2 * pFail));
	            }
	        }

	        if (toskkProps.isOoCShredOnly()) {
	            oocProcReturn = shredCost * ((1 - pFail) + 0.2 * pFail);
	        } else {
	            oocProcReturn = ((mangleCost * numberMangles + rakeCost * numberRakes + shredCost * numberShreds + ripCost * numberRips + biteCost * numberBites) * ((1 - pFail) + 0.2 * pFail)) / (numberMangles + numberRakes + numberShreds + numberRips + numberBites);
	        }

	        if (druid.getTalent(Talent.BERSERK) > 0) {
	            if (toskkProps.isOoCShredOnly()) {
	                oocBerserkProcReturn = shredCost / 2 * ((1 - pFail) + 0.2 * pFail);
	            } else {
	                oocBerserkProcReturn = ((mangleCost / 2 * numberMangles + rakeCost / 2 * numberRakes + shredCost / 2 * numberShreds + ripCost / 2  * numberRips + biteCost / 2 * numberBites) * ((1 - pFail) + 0.2 * pFail)) / (numberMangles + numberRakes + numberShreds + numberRips + numberBites);
	            }

	            oocProcReturn = (oocProcReturn * (encounterDuration - berserkUpDuration) + oocBerserkProcReturn * berserkUpDuration) / encounterDuration;
	        }

	        cycleEnergyRegenRate = cycleEnergyRegenRate + oocPPM / 60 * oocProcReturn;
	        energyLeft = cycleTime * cycleEnergyRegenRate - energyCost;

	        if (energyLeft > 0) {
	            if (toskkProps.isUseMangle()) {
	                numberMangles = energyLeft / (mangleCost * ((1 - pFail) + 0.2 * pFail));
	            } else {
	                numberShreds = energyLeft / (shredCost * ((1 - pFail) + 0.2 * pFail));
	            }
	        }

	        numberAttacks = numberMangles + numberRakes + numberShreds;
	        if (numberAttacks < requiredAttacks) {
	            cycleWarning = true;
	        }

	        double totalAttacks = (cycleTime / getMeleeInterval() + numberFurySwipes + numberMangles + numberShreds + numberRakes + numberRips + numberBites) * (1 - pFail);

	        // Save off the values worth keeping.
	        rval.cycleTime = cycleTime;

	        rval.energyRegenRate = cycleEnergyRegenRate;
	        rval.oocProcReturn = oocProcReturn;

	        rval.numberWhites = (cycleTime / getMeleeInterval()) * (1 - pFail);
	        rval.numberBites = numberBites;
	        rval.numberFurySwipes = numberFurySwipes;
	        rval.numberMangles = numberMangles;
	        rval.numberRakes = numberRakes;
	        rval.numberRips = numberRips;
	        rval.numberRoars = numberRoars;
	        rval.numberShreds = numberShreds;

	        rval.mangleUptime = mangleUptime;
	        rval.rakeUptime = rakeUptime;
	        rval.ripUptime = ripUptime;
	        rval.roarUptime = roarUptime;
	        rval.roarCp = roarCp;

	        rval.cycleWarning = cycleWarning;
	        rval.numberAttacks = numberAttacks;
	        rval.requiredAttacks = requiredAttacks;

	        rval.totalAttacks = totalAttacks;
	        rval.tier_11_4pcUptime = tier_11_4pcUptime;

	        return rval;
	    }

	    class CycleDPS {
	        double dps = 0;
	        double totalDamage = 0;
	        double damageBite = 0;
	        double numberBites = 0;

	        double dpsBase = 0;
	        double totalDamageBase = 0;
	        double numberBitesBase = 0;
	        double damageMelee = 0;
	        double damageFurySwipes = 0;
	        double damageRavage = 0;
	        double damageMangle = 0;
	        double damageShred = 0;
	        double damageBiteBase = 0;
	        double damageRake = 0;
	        double damageRip = 0;
	        double damageOpenerRavage = 0;
	        double damageSustainedRavage = 0;

	        double rakeSustainedUptime = 0;
	        double ripSustainedUptime = 0;

	        double dpsBitW = 0;
	        double totalDamageBitW = 0;
	        double damageBiteBitW = 0;
	        double numberBitesBitW = 0;

	        double pRavageCrit = 0;
	        double pBiteCrit = 0;
	        double modArmor = 0;
	        double debuffedArmor = 0;
	    }

	    /**
	     * Calclate DPS for a specified cycle.
	     */
	    private CycleDPS calculateCycleDPS(CycleResults cycle) {
	        CycleDPS rval = new CycleDPS();

	        double pCrit = getMeleeCritChance();

	        // Values we want to pass back to the user.
	        double dps = 0;
	        double totalDamage = 0;
	        double damageBite = 0;
	        double numberBites = 0;

	        double totalDamageBase = 0;
	        double damageMelee = 0;
	        double damageFurySwipes = 0;
	        double damageRavage = 0;
	        double damageMangle = 0;
	        double damageShred = 0;
	        double damageBiteBase = 0;
	        double damageRake = 0;
	        double damageRip = 0;
	        double damageOpenerRavage = 0;
	        double damageSustainedRavage = 0;

	        double damageProcPhys = 0;
	        double damageProcSpell = 0;

	        double pRavageCrit = 0;
	        double pBiteCrit = 0;

	        double totalDamageFof = 0;
	        double damageBiteFof = 0;
	        double damageRipFof = 0;
	        double numberBitesFof = 0;

	        // Formulate Tigers Fury.
	        tigersFuryUpDuration = GetCooldownUptime(tigersFuryBuffDuration, tigersFuryCDDuration, encounterDuration);
	        tigersFuryUptime = (double) tigersFuryUpDuration / encounterDuration;
	        double modTigersFury = multTigersFury * tigersFuryUptime + (1 - tigersFuryUptime);

	        // Tier 11 4pc.
	        // XXX: This needs to factor in ramp up.
	        if (druid.tier_11_4pc == true) {
	            tier_11_4pc_buffStacks = cycle.tier_11_4pcUptime * 3;
	            if (cycle.tier_11_4pcUptime < 1.0) {
	                this.view.onWarn("Tier 11 4pc projected uptime is insufficient for accurate modeling.");
	                this.view.onWarn("Tier 11 4pc Uptime:" + round(cycle.tier_11_4pcUptime));
	            }
	        }

	        // Calculate effective stats.
//	        calculateEffectiveStats();

	        // Derive Yellow Damage.
	        // Note: Assuming 5 CPs for finishers.
	        double ravageDmg = getWeaponDamage() * levelDep.getRavageMult() + levelDep.getRavageDmg();
	        double mangleDmg = getWeaponDamage() * levelDep.getMangleMult() + levelDep.getMangleDmg();
	        double shredDmg = getWeaponDamage() * levelDep.getShredMult() + levelDep.getShredDmg();
	        double rakeInitialDmg = levelDep.getRakeDmg() + getAttackPower() * levelDep.getRakeDmgCoeff();
	        double rakeTickDmg = levelDep.getRakeDotDmg() + getAttackPower() * levelDep.getRakeDotCoeff();
	        double biteDmg = levelDep.getFBBaseDmg() + levelDep.getFBCpDmg() * 5 + getAttackPower() * levelDep.getFBCoeff() * 5;
	        double ripTickDmg = levelDep.getRipBaseDmg() + levelDep.getRipCpDmg() * 5 + getAttackPower() * levelDep.getRipCoeff() * 5;
	        double ripTickFoFDmg = ripTickDmg - bugBloodInTheWaterRipDmg * 5;

	        mangleDmg *= ((druid.hasGlyph(Glyph.MANGLE)) ? 1.1 : 1);
	        biteDmg *= multBiteFeralAggression;

	        // Calculate mob armor.
	        if (buffsDebuffs.isDebuff(Debuff.ARMOR) == true) {
	            calculateTargetArmor(3, buffsDebuffs.isDebuff(Debuff.SHATTERINGTHROW));
	        } else {
	            calculateTargetArmor(0, buffsDebuffs.isDebuff(Debuff.SHATTERINGTHROW));
	        }

	        // Derive ability specific crit rates.
	        double pBleeding = 1 - ((1 - cycle.rakeUptime) * (1 - cycle.ripUptime));
	        pBleeding = ((toskkProps.isAssumeExternalBleed()) ? 1 : pBleeding);

	        pBiteCrit = pCrit + pBleeding * pBonusBiteCrit;
	        pBiteCrit = Math.min(1, pBiteCrit);

	        double pRipCrit = pCrit;
	        double pRakeCrit = pCrit;

	        pRavageCrit = Math.min(1, pCrit + (druid.getTalent(Talent.PREDATORYSTRIKES) * 0.25));

	        double rakeSustainedUptime = 0.0;
	        double ripSustainedUptime = 0.0;

	        // Tricks of the Trade
	        int tottUpDuration = GetCooldownUptime(tottBuffDuration, tottCDDuration, encounterDuration);
	        double tottUptime = (double) tottUpDuration / encounterDuration;
	        double buffTricksOfTheTrade = ((buffsDebuffs.isBuff(Buff.TRICKSOFTHETRADE)) ? (1.15 * tottUptime + (1 - tottUptime)): 1);

	        //
	        // On to the actual calculations.
	        // Note that we deal with flat damage multipliers at the very end.
	        //

	        // Figure out GCD requirements now.
	        //
	        // Note: I assume that the druid will open with:
	        // FF -> Mangle -> SR-> Rake -> Shred (to 5xCPs) -> Rip, with 2/2 Feral Aggression.
	        //
	        // Feral FF has a cooldown so it's not qute as simple as saying "FF to 3x." since I'd need to weave it,
	        // but it's not like I'm recalculating multTargetArmor anyway.
	        //
	        // Ravage->SR->FF-> etc was a net loss due to rake uptime loss.
	        double openerFfGCDs = 0;
	        double openerSrGCDs = 0;
	        double openerGCDs = 0;
	        double ripSustainedTime = 0;
	        if (toskkProps.isModelOpener()) {
	            if (toskkProps.isAssumeExternalFF() == false) {
	                // Feral FF for some absolutely *retarded* reason is on the spell hit table.
	                openerFfGCDs = Math.ceil(3 / nrFeralFFStacksPerCast) * (1 / (1 - getSpellMissChance()));
	            }
	            openerSrGCDs = 1 / (1 - pFail) + 1;
	            openerGCDs = openerFfGCDs + openerSrGCDs;
	        }

	        // Calculate white damage.
	        // TODO: Recheck glancing blow penalty.
	        if (toskkProps.isModelOpener()) {
	            // XXX: Armor mitigation is also going to be slightly different for the first few autos.
	            double meleeAttacks = encounterDuration / getMeleeInterval();
	            double meleeOpenerAttacks = openerGCDs / getMeleeInterval();
	            double meleeSustainedAttacks = meleeAttacks - meleeOpenerAttacks;
	            damageMelee = ((meleeOpenerAttacks * getWeaponDamage() * (0.75 * levelDep.getPGlance() + ((1 - levelDep.getPGlance()) - pFail - getWhiteCritChance()) + multCrit * getWhiteCritChance())) + (meleeSustainedAttacks * getWeaponDamage() * (0.75 * levelDep.getPGlance() + ((1 - levelDep.getPGlance()) - pFail - getWhiteCritChance()) + multCrit * getWhiteCritChance()) * multRoar)) / (encounterDuration / cycle.cycleTime);
	        } else {
	            damageMelee = cycle.cycleTime / getMeleeInterval() * getWeaponDamage() * (0.75 * levelDep.getPGlance() + ((1 - levelDep.getPGlance()) - pFail - getWhiteCritChance()) + multCrit * getWhiteCritChance()) * multRoar;
	        }

	        // Calculate Fury Swipes damage.
	        //
	        // XXX: In theory there is a slight chance that this will proc before
	        // we get Feral FF up.  In practice, the talent sucks regardless so it's
	        // not worth accounting for.
	        //
	        // Note: Build #12759. Savage Roar does NOT affect this.
	        damageFurySwipes = cycle.numberFurySwipes * (1 - pFail) * getWeaponDamage() * multFurySwipes * ((1 - pCrit) + multCrit * (pCrit));

	        // Calculate Mangle damage.
	        damageMangle = cycle.numberMangles * (1 - pFail) * mangleDmg * ((1 - pCrit) + multCrit * (pCrit));

	        // Calculate Shred damage.
	        //
	        // XXX: I'm fairly certain that nothing is needed for this when modeling opener.
	        // FFx->Mangle->SR->Rake->Shred means that we will have Mangle, and
	        // a bleed present on the mob.
	        damageShred = cycle.numberShreds * (1 - pFail) * shredDmg * ((1 - pCrit) + multCrit * (pCrit));
	        damageShred *= (cycle.mangleUptime * multMangle + (1 - cycle.mangleUptime));
	        damageShred *= (pBleeding * multShredRnT + (1 - pBleeding));

	        // Calculate Rake damage.
	        if (cycle.numberRakes > 0) {
	            double bRakeCrit = (1 - pRakeCrit) + multCrit * pRakeCrit;

	            double damageRakeBleed = 0;

	            damageRake = cycle.numberRakes * (1 - pFail) * rakeInitialDmg * (cycle.mangleUptime * multMangle + (1 - cycle.mangleUptime)) * ((1 - pCrit) + multCrit * pCrit);
	            damageRakeBleed = cycle.numberRakes * (1 - pFail) * rakeTickDmg * rakeTicks  * (cycle.mangleUptime * multMangle + (1 - cycle.mangleUptime));
	            damageRakeBleed *= bRakeCrit;
	            damageRake *= tier_10_4pc_rakeMult;
	            damageRakeBleed *= tier_10_4pc_rakeMult;
	            damageRakeBleed *= tier_11_2pc_rakeMult;

	            damageRake += damageRakeBleed;

	            if (toskkProps.isModelOpener()) {
	                double rakeOpenerDowntime = 1 / (1 - pFail) + openerGCDs;
	                double rakeSustainedTime = encounterDuration - rakeOpenerDowntime;
	                damageRake = damageRake * (rakeSustainedTime / encounterDuration);
	                rakeSustainedUptime = cycle.rakeUptime * (rakeSustainedTime / encounterDuration);
	            }
	        }

	        // Calculate Rip damage.
	        damageRip = ripTicks * ripTickDmg * ((1 - pRipCrit) + multCrit * pRipCrit);
	        damageRip *= (cycle.mangleUptime * multMangle + (1 - cycle.mangleUptime));
	        damageRip *= cycle.numberRips * (1 - pFail);
	        damageRip *= multRipGlyph;

	        // Feast of Flesh Rip damage.
	        // FIXME: This needs to be corrected to calculate Blood in the Water time "correctly" (See hack in the bite calculation).
	        // This is separated out now due to Bug.BloodInTheWaterRipDmg
	        damageRipFof = ripTicks * ripTickFoFDmg * ((1 - pRipCrit) + multCrit * pRipCrit);
	        damageRipFof *= (cycle.mangleUptime * multMangle + (1 - cycle.mangleUptime));
	        damageRipFof *= cycle.numberRips * (1 - pFail);
	        damageRipFof *= multRipGlyph;

	        if (toskkProps.isModelOpener()) {
	            double ripRequiredAttacks = (5 - 4 * getMeleeCritChance() + 3 * Math.pow(getMeleeCritChance(),2) - 2 * Math.pow(getMeleeCritChance(),3) + Math.pow(getMeleeCritChance(),4)) / (1 - pFail) - (1 / (1 - pFail));
	            double ripOpenerDowntime = 0;
	            if (toskkProps.isUseMangle() == true) {
	                ripOpenerDowntime = mangleCost * ripRequiredAttacks * ((1 - pFail) * 0.2 * pFail) / cycle.energyRegenRate;
	            } else {
	                ripOpenerDowntime = shredCost * ripRequiredAttacks * ((1 - pFail) * 0.2 * pFail) / cycle.energyRegenRate;
	            }
	            ripSustainedTime = encounterDuration - ripOpenerDowntime - (2 / (1 - pFail) + openerGCDs);
	            ripSustainedUptime = cycle.ripUptime * (ripSustainedTime / encounterDuration);
	        }

	        // Calculate Bite damage.
	        damageBiteBase = cycle.numberBites * (1 - pFail) * biteDmg * ((1 - pBiteCrit) + multCrit * pBiteCrit);
	        if (druid.getTalent(Talent.BLOODINTHEWATER) > 0) {
	            // HACK: Support 0 or 2 points in Blood in the Water.
	            if (druid.getTalent(Talent.BLOODINTHEWATER) == 2) {
	                damageBiteFof = (cycle.numberBites + cycle.numberRips) * (1 - pFail) * biteDmg * ((1 - pBiteCrit) + multCrit * pBiteCrit);
	                numberBitesFof = cycle.numberBites + cycle.numberRips;
	            } else {
	                this.view.onWarn("Blood in the Water modeling requires 2 points in the talent.");
	            }
	        }

	        if (toskkProps.isModelOpener()) {
	            // Yes this is after Blood in the Water processing since ramp up has 0 impact on damage there.
	            double biteSustainedTime = ripSustainedTime;
	            damageBiteBase = damageBiteBase * (biteSustainedTime / encounterDuration);
	        }

	        // Calculate Ravage damage.
	        // Note that attacks from stealth are unavoidable.  (Failure is Miss only.)
	        damageOpenerRavage = 1 * (1 - getMissChance()) * ravageDmg * ((1 - pRavageCrit) + multCrit * pRavageCrit);
	        damageSustainedRavage = 1 * (1 - pFail) * ravageDmg * ((1 - pRavageCrit) + multCrit * pRavageCrit);

	        // Calculate Proc damage.
	        damageProcPhys = (1 - pFail) * procDamagePhys * ((1 - getMeleeCritChance()) + multCrit * pCrit);
	        damageProcSpell = (1 - getSpellMissChance()) * procDamageSpell * ((1 - getSpellCritChance()) + multSpellCrit * getSpellCritChance());

	        // Factor in Armor, Static damage multipliers.
	        damageMelee *= (1 - multTargetArmor) * modTigersFury * multPhysVuln * multDamageMult * buffTricksOfTheTrade;
	        damageFurySwipes *= (1 - multTargetArmor) * modTigersFury * multPhysVuln * multDamageMult * buffTricksOfTheTrade;
	        damageMangle *= (1 - multTargetArmor) * modTigersFury * multPhysVuln * multDamageMult * buffTricksOfTheTrade;
	        damageShred *= (1 - multTargetArmor) * modTigersFury * multPhysVuln * multDamageMult * buffTricksOfTheTrade;
	        damageBiteBase *= (1 - multTargetArmor) * modTigersFury * multPhysVuln * multDamageMult * buffTricksOfTheTrade;
	        damageBiteFof *= (1 - multTargetArmor) * modTigersFury * multPhysVuln * multDamageMult * buffTricksOfTheTrade;
	        damageRake *= multPhysVuln * multDamageMult * modTigersFury * getBleedMastery() * buffTricksOfTheTrade * multBleedDarkIntent;
	        damageRip *= multPhysVuln * multDamageMult * modTigersFury * getBleedMastery() * buffTricksOfTheTrade * multBleedDarkIntent;
	        damageRipFof *= multPhysVuln * multDamageMult * modTigersFury * getBleedMastery() * buffTricksOfTheTrade * multBleedDarkIntent;
	        damageOpenerRavage *= (1 - multTargetBaseArmor) * modTigersFury * multPhysVuln * multDamageMult * buffTricksOfTheTrade;
	        damageSustainedRavage *= (1 - multTargetArmor) * modTigersFury * multPhysVuln * multDamageMult * buffTricksOfTheTrade;

	        damageProcPhys *= (1 - multTargetArmor) * modTigersFury * multPhysVuln * multDamageMult * buffTricksOfTheTrade;
	        damageProcSpell *= multDamageMult * buffTricksOfTheTrade;

	        // Factor in Stampede here.  (Only want to display Ravage metrics if Stampede is present).
	        if (druid.getTalent(Talent.STAMPEDE) == 0) {
	        	damageOpenerRavage = 0;
	        	damageSustainedRavage = 0;
	        } else {
	        	// You get one Ravage, that you can weave into your opener, so interpolate it.
	        	damageRavage = damageSustainedRavage / encounterDuration * cycle.cycleTime;
	        }

	        totalDamageBase = damageMelee + damageFurySwipes + damageRavage + damageMangle + damageShred + damageBiteBase + damageRake + damageRip + damageProcPhys + damageProcSpell;
	        totalDamageFof = damageMelee + damageFurySwipes + damageRavage + damageMangle + damageShred + damageBiteFof + damageRake + damageRipFof + damageProcPhys + damageProcSpell;

	        // Interpolate Blood in the Water data.
	        if (druid.getTalent(Talent.BLOODINTHEWATER) == 2 && bitwTime > 0) {
	            double nTime = bitwTime;

	            dps = totalDamageBase / cycle.cycleTime * (1 - nTime) + totalDamageFof / cycle.cycleTime * nTime;
	            totalDamage = totalDamageBase * (1 - nTime) + totalDamageFof * nTime;
	            damageBite = damageBiteBase * (1 - nTime) + damageBiteFof * nTime;
	            numberBites = cycle.numberBites * (1 - nTime) + numberBitesFof * nTime;
	        } else {
	            dps = totalDamageBase / cycle.cycleTime;
	            totalDamage = totalDamageBase;
	            damageBite = damageBiteBase;

	            numberBites = cycle.numberBites;
	        }

	        // Save off values we care about.
	        rval.dps = dps;
	        rval.damageBite = damageBite;
	        rval.totalDamage = totalDamage;
	        rval.numberBites = numberBites;

	        rval.dpsBase = totalDamageBase / cycle.cycleTime;
	        rval.totalDamageBase = totalDamageBase;
	        rval.damageMelee = damageMelee;
	        rval.damageFurySwipes = damageFurySwipes;
	        rval.damageRavage = damageRavage;
	        rval.damageMangle = damageMangle;
	        rval.damageShred = damageShred;
	        rval.damageBiteBase = damageBite;
	        rval.damageRake = damageRake;
	        rval.damageRip = damageRip;
	        rval.damageOpenerRavage = damageOpenerRavage;
	        rval.damageSustainedRavage = damageSustainedRavage;
	        rval.numberBitesBase = cycle.numberBites;

	        rval.rakeSustainedUptime = rakeSustainedUptime;
	        rval.ripSustainedUptime = ripSustainedUptime;

	        rval.dpsBitW = totalDamageFof / cycle.cycleTime;
	        rval.totalDamageBitW = totalDamageFof;
	        rval.damageBiteBitW = damageBiteFof;
	        rval.numberBitesBitW = numberBitesFof;

	        rval.pRavageCrit = pRavageCrit;
	        rval.pBiteCrit = pBiteCrit;
	        rval.modArmor = multTargetArmor;
	        rval.debuffedArmor = targetArmor;

	        return rval;
	    }

	    /**
	     * Factors various procs into our stats.
	     */
	    private void calculateProcEffects(CycleResults results)
	    {
	        Procs procsInstance = Procs.getInstance();
	        List<ProcEffect> procEffects = null;

	        procEffects = procsInstance.getProcsByProperty(toskkProps.getProperties());

	        for (Iterator<ProcEffect> iter = procEffects.iterator(); iter.hasNext(); ) {
	            ProcEffect effect = iter.next();

	            // Figure out uptime.
	            double uptime = 0;
	            double procRate = Math.max(effect.rate, effect.ppm * (1.0 / 60));
	            double nrTriggers = 0;

	            if (effect.triggers.equals(EnumSet.of(ProcEffect.Trigger.USER))) {
	                uptime = (double) effect.buffDuration / effect.buffCooldown;
	            } else if (effect.triggers.equals(EnumSet.of(ProcEffect.Trigger.POTION))) {
	            	uptime = (double) effect.buffDuration / encounterDuration;
	            } else {
	                double nrYellowAttacks = results.numberFurySwipes + results.numberMangles + results.numberRakes + results.numberShreds + results.numberRips + results.numberBites;
	                double nrDotTicks = results.numberRakes * (1 - pFail) * rakeTicks + ripTicks;

	                for (ProcEffect.Trigger t : effect.triggers) {
	                    switch (t) {
	                        case POTION:
	                        	// Should never happen.
	                        	break;
	                        case USER:
	                            // Should never happen.
	                            break;
	                        case WHITE:
	                            nrTriggers += results.numberWhites * (1 - getWhiteCritChance());
	                            break;
	                        case WHITECRIT:
	                            nrTriggers += results.numberWhites * getWhiteCritChance();
	                            break;
	                        case YELLOW:
	                            nrTriggers += nrYellowAttacks * (1 - getMeleeCritChance());
	                            break;
	                        case YELLOWCRIT:
	                            nrTriggers += nrYellowAttacks * getMeleeCritChance();
	                            break;
	                        case DOT:
	                            nrTriggers += nrDotTicks * (1- getMeleeCritChance());
	                            break;
	                        case DOTCRIT:
	                            nrTriggers += nrDotTicks * getMeleeCritChance();
	                            break;
	                    }
	                }

	                nrTriggers /= results.cycleTime;

	                uptime = GetProcUptime(procRate, nrTriggers, effect.buffDuration, effect.buffCooldown);
	            }

	            // Calculate the effect.
	            double procMult = Math.max(effect.buffStackMax, 1);
	            if (effect.buffStackMax > 1) {
	                if (uptime < 1.0) {
	                    // Ok, the proc has non-100% uptime, and stacks, I need to take this into account.
	                    if (effect.get(Effect.SPELL_ID) == 0) {
	                        //
	                        // These need to be special cased.  I doubt anyone still uses a BNS, and
	                        // Victor's Call is garbage so just print out a warning for now.
	                        //
	                        this.view.onWarn("Proc effect \"" + effect.name + "\" not supported. (Stacks and has non-100% uptime)");
	                        continue;
	                    }
	                } else {
	                    if (toskkProps.isModelOpener() == true) {
	                        // Ok, it has 100% uptime, but I need to model ramp if we care about the opener.
	                        if (nrTriggers == 0 || procRate == 0) {
	                            // I'm not sure if this will ever occur.
	                            this.view.onWarn("Proc effect \"" + effect.name + "\" not supported. (Has ramp up time, opener modeling requested.)");
	                            continue;
	                        }

	                        double stackTime = effect.buffStackMax / (nrTriggers * procRate);
	                        double stackInterval = 1.0 / (nrTriggers * procRate);

	                        double n = effect.buffStackMax - 1;
	                        n = (n * (n + 1) / 2);

	                        procMult = (encounterDuration - stackTime) * effect.buffStackMax + stackInterval * n;
	                        procMult /= encounterDuration;
	                    } else {
	                        // Only multiplier that affects this is uptime, since we do not care about ramp duration.
	                        procMult *= uptime;
	                    }
	                }
	            } else {
	                // Everything else just gets a multiplier that is uptime based.
	                procMult *= uptime;
	            }

	            if (effect.get(Effect.SPELL_ID) != 0) {
	                // Proc effects that require special handling go here.
	                if (effect.get(Effect.SPELL_ID) == 50363) {
	                    // Deathbringer's Will (277)
	                    procMult = procMult / 3; /* Randomly picks out of the 3 procs. */
	                    bonusStrength += 700 * procMult;
	                    bonusHasteRating += 700 * procMult;
	                    bonusAgility += 700 * procMult;
	                } else if (effect.get(Effect.SPELL_ID) == 50362) {
	                	// Deathbringer's Will (264)
	                    procMult = procMult / 3; /* Randomly picks out of the 3 procs. */
	                    bonusStrength += 600 * procMult;
	                    bonusHasteRating += 600 * procMult;
	                    bonusAgility += 600 * procMult;
	                } else if (effect.get(Effect.SPELL_ID) == 26297) {
	                	// Troll Berserking
	                	hasteMult *= 1.2 * procMult + (1 - procMult);
	                } else {
	                    this.view.onWarn("Proc effect \"" + effect.name + "\" not supported.  (Requires special handling.)");
	                    continue;
	                }
	            } else {
	                if (effect.get(Effect.STRENGTH) > 0) {
	                    bonusStrength += effect.get(Effect.STRENGTH) * procMult;
	                }
	                if (effect.get(Effect.AGILITY) > 0) {
	                    bonusAgility += effect.get(Effect.AGILITY) * procMult;
	                }
	                if (effect.get(Effect.ATTACK_POWER) > 0) {
	                    bonusAttackPower += effect.get(Effect.ATTACK_POWER) * procMult;
	                }
	                if (effect.get(Effect.HASTE_RATING) > 0) {
	                    bonusHasteRating += effect.get(Effect.HASTE_RATING) * procMult;
	                }
	                if (effect.get(Effect.CRIT_RATING) > 0) {
	                    bonusCritRating += effect.get(Effect.CRIT_RATING) * procMult;
	                }
	                if (effect.get(Effect.MASTERY_RATING) > 0) {
	                    bonusMasteryRating += effect.get(Effect.MASTERY_RATING) * procMult;
	                }

	                // TODO: Procing hit/expertise rating is somewhat nonsensical, I should
	                // handle this better.
	                if (effect.get(Effect.HIT_RATING) > 0) {
	                    bonusHitRating += effect.get(Effect.HIT_RATING) * procMult;
	                }
	                if (effect.get(Effect.EXPERTISE_RATING) > 0) {
	                    bonusExpertiseRating += effect.get(Effect.EXPERTISE_RATING) * procMult;
	                }

	                if (effect.get(Effect.PHYSICAL_DAMAGE) > 0) {
	                    procDamagePhys += effect.get(Effect.PHYSICAL_DAMAGE) * procMult;
	                }
	                if (effect.get(Effect.SPELL_DAMAGE) > 0) {
	                    procDamageSpell += effect.get(Effect.SPELL_DAMAGE) * procMult;
	                }
	            }

	            // Store the uptime statistics.
	            if (procUptimes == null) {
	                procUptimes = new HashMap<ProcEffect, Double>();
	            }
	            procUptimes.put(effect, uptime);
	        }
	    }

	    /**
	     * Generates a human readable report from the DPS figures.
	     */
	    private CatReport generateReport(CycleResults cycle, CycleDPS cycleDps)
	    {
	        // XXX: Not very efficient like this, probably better to instantiate the CatReport first
	        // and use the variables there directly, but this model is lightning fast compared to the
	        // alternative so it probably does not matter much.

	        CatReport report = new CatReport();

	        //report.mDps = cycleDPS.dps;						// DPS
	        if (druid.getTalent(Talent.BLOODINTHEWATER) == 2 && bitwTime > 0) {
	            report.mDpsBase = cycleDps.dpsBase;				// DPS > 25%
	            report.mDpsBitW = cycleDps.dpsBitW;				// DPS < 25%
	        } else {
	            report.mDpsBase = 0;
	            report.mDpsBitW = 0;
	        }
	        report.pFail = pFail;								// pMiss + Dodge
	        report.pWhiteCrit = getWhiteCritChance();						// White crit rate
	        report.pYellowCrit = getMeleeCritChance();					// Yellow crit rate (Base, Bite handled elsewhere.)
	        report.pWhiteCritCap = getWhiteCritCap();				// White crit cap
	        report.mSwingTimer = getMeleeInterval();					// Swing timer
	        report.mOoCPPM = oocPPM;							// Omen of Clarity PPM
	        report.mOoCReturn = cycle.oocProcReturn;			// Energy/OoC Proc
	        report.mEnergyRegenRate = cycle.energyRegenRate;	// Energy regeneration rate

	        report.mNrSavageRoar = cycle.numberRoars;			// Number of Savage Roars
	        report.mSavageRoarUptime = cycle.roarUptime;		// Savage Roar Uptime
	        report.mSavageRoarCPs = cycle.roarCp;				// Savage Roar CP/cast

	        report.mNrMangles = cycle.numberMangles;			// Number of Mangles
	        report.mMangleUptime = cycle.mangleUptime;			// Mangle debuff uptime

	        report.mNrShreds = cycle.numberShreds;				// Number of Shreds

	        report.mNrRakes = cycle.numberRakes;				// Number of Rakes
	        if (toskkProps.isModelOpener() == true) {
	            report.mRakeUptime = cycleDps.rakeSustainedUptime;	// Rake DoT uptime
	        } else {
	            report.mRakeUptime = cycle.rakeUptime;
	        }

	        report.mNrRips = cycle.numberRips;					// Number of Rips
	        if (toskkProps.isModelOpener() == true) {
	            report.mRipUptime = cycleDps.ripSustainedUptime;// Rip DoT uptime
	        } else {
	            report.mRipUptime = cycle.ripUptime;
	        }

	        report.mNrBites = cycleDps.numberBites;				// Number of Bites
	        if (druid.getTalent(Talent.BLOODINTHEWATER) == 2 && bitwTime > 0) {
	            report.mNrBitesBase = cycleDps.numberBitesBase;	// Number of Bites > 25%
	            report.mNrBitesPerRip = cycleDps.numberBitesBase * (1 - pFail);	// Number of Bites per Rip (> 25%)
	            report.mNrBitesBitW = cycleDps.numberBitesBitW;	// Number of Bites < 25%
	        } else {
	            report.mNrBitesBase = 0;						// Number of Bites > 25%
	            report.mNrBitesPerRip = cycle.numberBites * (1 - pFail);	// Number of Bites per Rip (> 25%)
	            report.mNrBitesBitW = 0;							// Number of Bites < 25%
	        }
	        report.pBiteCrit = cycleDps.pBiteCrit;				// Bite crit

	        report.mNrBerserks = cycle.cycleTime / berserkCDDuration;
	        report.mBerserkUptime = berserkUptime; // XXX: Should I use (int) Math.floor(berserkCDDuration / encounterDuration) + 1???

	        report.mNrTigersFurys = cycle.cycleTime / tigersFuryCDDuration;
	        report.mTigersFuryUptime = tigersFuryUptime;

	        report.mSegmentTime = cycle.cycleTime;				// Segment time (Encounter duration for sim.)

	        report.mArmorValue = cycleDps.debuffedArmor;		// Armor value
	        report.mArmorMitigation = cycleDps.modArmor;		// Armor mitigation (%)

	        report.pWhiteDamage = cycleDps.damageMelee / cycleDps.totalDamage;		// White Damage (%)
	        report.pFurySwipesDamage = cycleDps.damageFurySwipes / cycleDps.totalDamage;	// Fury Swipes Damage (%)
	        report.pRavageDamage = cycleDps.damageRavage / cycleDps.totalDamage;	// Ravage Damage (%)
	        report.pMangleDamage = cycleDps.damageMangle / cycleDps.totalDamage;	// Mangle Damage (%)
	        report.pShredDamage = cycleDps.damageShred / cycleDps.totalDamage;		// Shred Damage (%)
	        report.pRakeDamage = cycleDps.damageRake / cycleDps.totalDamage;		// Rake Damage (%)
	        report.pRipDamage = cycleDps.damageRip / cycleDps.totalDamage;			// Rip Damage (%)
	        report.pBiteDamage = cycleDps.damageBite / cycleDps.totalDamage;		// Bite Damage (%)
	        if (procDamagePhys > 0 || procDamageSpell > 0)
	        	report.pOtherDamage = 1 - (report.pWhiteDamage + report.pFurySwipesDamage + report.pMangleDamage + report.pShredDamage + report.pRakeDamage + report.pRipDamage + report.pBiteDamage + report.pRavageDamage);		// Other Damage (%) (Procs)

			// TODO: Find a cleaner way to deal with warnings without going through MewView?
			if (cycle.cycleWarning) {
				this.view.onWarn("Entered stats do not give Savage Roar enough combo points for the model to be 100% accurate.");
				this.view.onWarn("CP-Giving Attacks: " + round(cycle.numberAttacks));
				this.view.onWarn("CP-Giving Attacks Required: " + round(cycle.requiredAttacks));
			}

	        report.mAvgWhiteAttack = cycleDps.damageMelee / (cycle.cycleTime / getMeleeInterval());		// Average white attack.
	        report.mAvgRavageAttack = cycleDps.damageSustainedRavage;							// Average Ravage attack.
	        report.mMangleDPE = cycleDps.damageMangle / (cycle.numberMangles * mangleCost * ((1 - pFail) + 0.2 * pFail));	// Mangle damage/energy
	        report.mShredDPE = cycleDps.damageShred / (cycle.numberShreds * shredCost * ((1 - pFail) + 0.2 * pFail));		// Shred damage/energy
	        report.mRakeDPE = cycleDps.damageRake / (cycle.numberRakes * rakeCost * ((1 - pFail) + 0.2 * pFail));			// Rake damage/energy
	        report.mRipDPE = cycleDps.damageRip / (cycle.numberRips * ripCost * ((1 - pFail) + 0.2 * pFail));				// Rip damage/energy
	        report.mBiteDPE = cycleDps.damageBiteBase / (cycleDps.numberBitesBase * biteCost * ((1 - pFail) + 0.2 * pFail));			// Bite damage/energy

	        report.mProcUptimes = procUptimes;

	        return report;
	    }

	    /**
	     * Calculate cooldown uptime, assuming it is used at the earliest possible opportunity.
	     *
	     * @return Uptime in seconds.
	     */
	    private int GetCooldownUptime(int up, int down, int total) {
	        int fullUses = total/down;
	        int timeLeft = total - fullUses * down;
	        int partialUp = (timeLeft >= up) ? up : timeLeft;

	        return up * fullUses + partialUp;
	    }

	    /**
	     * Calculate proc uptime %.
	     *
	     * @return % uptime.
	     */
	    private double GetProcUptime(double procRate, double numTriggers, double buffDuration, double icd) {
	        double uptime = 0;

	        if (icd > 0) {
	            // Uptime = Duration / (ICD + 1 / (Triggers * Proc Rate))
	            uptime = buffDuration / (icd + 1 / (numTriggers * procRate));
	        } else {
	            // Uptime = 1 - (1 - Proc Rate) ^ (Duration * Triggers)
	            uptime = 1 - Math.pow((1 - procRate),(buffDuration * numTriggers));
	        }

	        return uptime;
	    }

	    @Override
	    public String toString() {
	        StringBuilder rval = new StringBuilder();
	        rval.append(super.toString());
	        rval.append(toskkProps.toString());
	        return rval.toString();
	    }
    }
}
